個人CodeBase紀錄 - EP.2 不想 Bind data 到吐,來自訂一下 Aspose 的擴充


Posted by Mike.Lin on 2023-08-07

公司專案的需求,常有將資料套進範例檔或是做報表的需求,若是每次套版都要重新寫迴圈、Bind資料,會吐,真的會吐TT。所以這篇除了aspose基本的使用,主軸會放在自訂擴充方法的部分。


首先,先來說明aspose。Aspose是一個開發文檔控制相關套件的公司,需要購買憑證才能有完整功能,套件提供了許多操控文檔結構、資料及樣式的實作。以下我會說明 .doc 及 .xlsx 在aspose中如何實作 bind data:

Aspose.Words

文檔控制在實作上,主要是找到對應欄位或位置,並對該位置作改動,而文檔會有不同的結構,如cells、tables、prograph等,不同的結構在Asp.Net可以當作物件,同時aspose提共了方法來操作這些結構。

doc文檔中,我們可以透過 Mergefield 來找到目標位置在文檔中的位置。如圖中所示,可以使用 word 中的插入功能變數來新增一個MergeField

並撰寫程式如下,將範例檔欄位名稱與資料作對應

Document doc = new Document("template.docx"); // 取得範例檔

// new 一個 DataTable 物件來接收資料
var table = new DataTable();
table.Columns.Add("UserName", string); // 給欄位名稱及型別
var row = table.NewRow(); 
row[0] = "Mike" // 給值
table.Rows.Add(row); // 加回 DataTable

// 進行郵件合併
doc.MailMerge.Execute(table);

// 存檔
doc.Save("merged.docx");

結果如下,可以看到UserName已經被取代為"Mike"

我們也可以透過bookmark的方式來控制文檔,但這部分並非此篇重點,就不細談。

Aspose.Cells

xlsx 文檔中,則是透過 "&=" 字符來標示欄位,後面接 table Name 及 colum Name

並撰寫程式如下,將範例檔欄位名稱與資料作對應

var workBook = new Workbook("test.xlsx"); // 取得範例檔
var designer = new WorkbookDesigner(); // designer 物件 用以處理文檔
designer.Workbook = workBook;

var test = new DataTable();
/*
datatable 部分
*/

// 給定 tablename
test.TableName = "data"; 

var dataSet = new DataSet(); 
dataSet.Tables.Add(test); // 將 datatable塞到dataset
designer.SetDataSource(dataSet); // 透過 designer 來確認資料來源

designer.Process(true); // binding

值得注意的是,xlsx 文檔內 所有物件的上層都會有 table,因此必須給範例檔中相同的 table name 才對應的到。

Bind 完會自動依資料筆數取代字符。資料及結果如下:

自訂擴充方法

在實作時,我們通常會有不同區塊的欄位需要填寫,有時某些資料會是列表,需要動態去作欄位的新增,所以共用方法必須考慮可以去解析.Net資料的結構,特別是像葡萄串一樣的資料。


DataTable

我預設將接近來的資料分為單筆及多筆,用不同方式去處理。

單筆部分

只取非泛型、非列表的欄位,並根據這些欄位及資料塞入datatable中

/// <summary>
/// 單一field bind 後回傳table
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <returns></returns>
private static DataTable GetBindDataSingleField<T>(T data)
{
    var table = new DataTable();
    // 取得資料的 名稱、屬性 列表 // 列表內資料非泛型、非列表
    var columnFieldList = data.GetType().GetProperties().Where(p => !(p.PropertyType.IsGenericType && p is IList)).Select(x => new { x.Name, x.PropertyType }).ToList();
    foreach (var column in columnFieldList)
    {
        // 塞到欄位中 類型為基礎類型或屬性的類型
        table.Columns.Add(column.Name, Nullable.GetUnderlyingType(column.PropertyType) ?? column.PropertyType);
    }

    var row = table.NewRow();
    // 取得資料的 名稱、值  列表
    var dataMapping = data.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(data, null)).ToList();
    // 取得資料的 值  列表
    var columnNameList = columnFieldList.Select(m => m.Name).ToList();
    foreach (var item in dataMapping)
    {
        // 名稱若有對到
        if (columnNameList.Any(x => x == item.Key))
        {
            // 名稱對應的row 塞值 // 找不到報null錯
            row[item.Key] = item.Value ?? DBNull.Value;
        }
    }
    table.Rows.Add(row);

    return table;
}

非單筆部分

逐筆處理資料為泛型、列表的資料,一樣根據這些欄位及資料塞入datatable中

/// <summary>
/// 列表field bind 後回傳table
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="fillNewRowWhenEmpty"></param>
/// <returns></returns>
private static List<DataTable> GetBindDataListField<T>(T data, bool fillNewRowWhenEmpty)
{
    var tables = new List<DataTable>();
    // 取得資料的 名稱、屬性 列表 // 列表內資料為泛型、列表
    var listTypeFields = data.GetType().GetProperties().Where(p => p.PropertyType.IsGenericType && p.GetValue(data, null) is IList).ToList();

    foreach (var item in listTypeFields)
    {
        var table = new DataTable
        {
            TableName = item.Name
        };
        // 取得屬性的類型泛型 非單一時報錯
        var type = item.PropertyType.GetGenericArguments().Single();
        // 取得資料的 名稱、屬性 列表
        var columnFieldList = type.GetProperties().Select(x => new { x.Name, x.PropertyType }).ToList();
        // List<T> 中取得 T 的 屬性名稱、類型
        foreach (var column in columnFieldList)
        {
            table.Columns.Add(column.Name, Nullable.GetUnderlyingType(column.PropertyType) ?? column.PropertyType);
        }

        // 取得列表值
        var rowDataSource = (IList)item.GetValue(data, null);

        foreach (var rowDataItemObj in rowDataSource)
        {
            var rowDataItem = Convert.ChangeType(rowDataItemObj, type);
            // 取得資料的 名稱、值  列表
            var dataMapping = rowDataItem.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(rowDataItem, null)).ToList();
            var row = table.NewRow();
            foreach (var fieldData in dataMapping)
            {
                // 名稱若有對到
                if (columnFieldList.Any(x => x.Name == fieldData.Key))
                {
                    // 名稱對應的row 塞值 // 找不到報null錯
                    row[fieldData.Key] = fieldData.Value ?? DBNull.Value;
                }
            }
            table.Rows.Add(row);
        }
        if (table.Rows.Count == 0 && fillNewRowWhenEmpty)
        {
            table.Rows.Add(table.NewRow());
        }
        tables.Add(table);
    }
    return tables;
}

完成以上兩個function,我們便有了可以將資料轉成 DataTable 的方法。接下來,由於 docx 與 xlsx 實作 binding 方式的不同,這邊分開來寫:

Document

/// <summary>
/// bind doc中同名欄位
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="doc"></param>
/// <param name="data"></param>
/// <param name="fillNewRowWhenEmpty"></param>
/// <returns></returns>
public static Document BindData<T>(this Document doc, T data, bool fillNewRowWhenEmpty = false)
{
    #region 郵件合併 doc內只能用 功能變數 -> MergyField
    // Data 類別第一層
    // 取得單一field bind後table
    var tableSingleField = GetBindDataSingleField(data);
    // doc跟bind後table合併
    doc.MailMerge.Execute(tableSingleField);

    // Data 類別第二層(List內的List)
    // 取得列表 field bind後table
    var tables = GetBindDataListField(data, fillNewRowWhenEmpty);
    // doc跟bind後table合併
    foreach (var item in tables)
    {
        doc.MailMerge.ExecuteWithRegions(item);
    }
    doc.Save("Test.docx", Aspose.Words.SaveFormat.Docx);
    #endregion
    #region ReportingEngine 可用 <<[]>> 較靈活 較簡單
    // 將模板文檔與生成report
    var engine = new ReportingEngine();
    engine.BuildReport(doc, data);
    #endregion
    return doc;
}

Workbook

/// <summary>
/// 把資料合併到 workBook 中同名的㯗位
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="doc"></param>
/// <param name="data"></param>
/// <returns></returns>
public static Workbook BindData<T>(this Workbook workBook, T data, bool fillNewRowWhenEmpty = true)
{
    var designer = new WorkbookDesigner();
    designer.Workbook = workBook;

    var dataSet = new DataSet();
    // Data 類別第一層
    // 取得單一field bind後table
    var test = GetBindDataSingleField(data);
    // 多設table名與第一層資料對應
    test.TableName = "data";

    dataSet.Tables.Add(test);
    designer.SetDataSource(dataSet);

    // Data 類別第二層(List內的List)
    // 取得列表 field bind後table
    var listTables = GetBindDataListField(data, fillNewRowWhenEmpty);
    dataSet.Tables.AddRange(listTables.ToArray());
    // set回 workbook
    designer.SetDataSource(dataSet);
    // binding // 保留計算並生成excel
    designer.Process(true);

    return workBook;
    }

使用自訂擴充作 Data Binding

完成以上,我們便可在web專案只考慮資料及範例檔,就能完成套印

/// <summary>
/// 取得doc檔匯出串流
/// </summary>
/// <returns></returns>
public ActionResult ExportDocxFile()
{
    // 假資料
    var data = _FakeMultiLayerList.FakeListForBind(); // 假資料

    var doc = new Document("test.docx"); // 開啟範例文檔
    doc.BindData(data); // 使用 自訂bind擴充
    doc.Save("bindedDoc.docx", Aspose.Words.SaveFormat.Docx);

    return File(doc.GetFileStream(Aspose.Words.SaveFormat.Docx), "application/docx");
}

/// <summary>
/// 取得xlsx檔匯出串流
/// </summary>
/// <returns></returns>
public ActionResult ExportXlsxFile()
{
    var data = _FakeMultiLayerList.FakeListForBind(); // 假資料

    var workBook = new Workbook("test.xlsx"); // 開啟範例文檔
    workBook.BindData(data); // 使用 自訂bind擴充
    workBook.Save("bindedDoc.xlsx", Aspose.Cells.SaveFormat.Xlsx);

    return File(workBook.GetFileStream(Aspose.Cells.SaveFormat.Xlsx), "application/xlsx");
}

結語

這邊的擴充,是考慮了我在工作上常用到的檔案套印,資料通常都是較扁平的,並且多數沒有作樣式的調整(樣式的部分通常為客製的,很難寫成通用方法),若先把擴充寫好,便只需考慮範例文檔的調整,省了很多事。


#ASP.NET #aspose.words #Aspose.Cells #notes







Related Posts

HTTP API 串接

HTTP API 串接

【JS幼幼班】Step.03 JavaScript 的基本概念

【JS幼幼班】Step.03 JavaScript 的基本概念

Secure Apache Using Certbot with Let's Encrypt on Ubuntu 20.04

Secure Apache Using Certbot with Let's Encrypt on Ubuntu 20.04


Comments